# asyncio.run()
# Вы уже использовали данную функцию для запуска корутин, являющихся точкой входа в ваше асинхронное приложение. 
# Рассмотрим функцию более подробно в данном уроке. 

# asyncio.run(coro, *, debug=None, loop_factory=None) — функция создаёт цикл событий и запускает указанную корутину в нём. 
# После завершения выполнения coro автоматически закрывает цикл событий. Эта функция является основным методом для запуска и 
# управления asyncio приложениями. Эту функцию нельзя вызывать, когда другой цикл событий asyncio уже работает в том же потоке.

#coro: Coroutine (корутина), которую нужно запустить. Это асинхронная функция, которую вы хотите выполнить.

# debug: (Необязательный) Логический флаг, указывающий, следует ли запускать цикл событий в режиме отладки. Если None, 
# значение берется из глобальной настройки отладки.

# loop_factory: (Необязательный) представляет собой вызываемый объект (функцию или класс с методом __call__), который возвращает
# экземпляр цикла событий. Это позволяет пользователям предоставлять свою реализацию цикла событий или настроенный экземпляр цикла 
# событий, который может иметь определенные настройки или расширения ( подробное описание с примером приведено в конце урока).

# Особенности asyncio.run()
# 1. Создает и закрывает цикл событий. Функция создаёт цикл событий и запускает указанный объект coroutine в нём. Эта функция является
# основным методом для запуска и управления asyncio приложениями. После завершения выполнения main() метод  asyncio.run() завершает цикл
# событий и все задачи в нем. Это позволяет изолировать выполнение различных асинхронных операций и управлять ими в контексте
# определенного цикла событий почти в автоматическом режиме.

# При этом все другие корутины, которые могли бы быть запущены в цикле событий, но не завершили свое выполнение до завершения main(),
# также завершат свою работу и выполнены до конца не будут. 

# 2. В рамках потока вызывается один раз. Так как функция является точкой входа для программ на asyncio, она должна вызываться только один раз,
# это связано с тем, что в каждом потоке может быть только один активный цикл событий. Однако вы можете использовать asyncio.run() в разных потоках
# с помощью asyncio.to_thread() (о которой мы будем говорить позже), потому что каждый поток может иметь свой собственный цикл событий 
# (работа с несколькими потоками вне рамок этого курса).

# 3. asyncio.run() нельзя вызвать из корутины. Это приведет к ошибке, так как функцию нельзя вызывать, когда другой цикл событий
# asyncio уже работает в том же потоке.

# import asyncio


# async def nested_run():
#     asyncio.run(await asyncio.sleep(1))

# async def main():
#     await nested_run()

# asyncio.run(main())

#RuntimeError: asyncio.run() cannot be called from a running event loop

# 4. Поддержка только корутин: asyncio.run() может быть использован только с корутинами. Если вы попытаетесь передать обычную функцию 
# вместо корутины, вы получите ошибку ValueError.

# 5. Порядок выполнения: asyncio.run(main()) не гарантирует порядок выполнения асинхронных операций внутри main(). Операции 
# вернут результат по факту своего завершения, и порядок завершения их работы далеко не всегда будет совпадать с порядком 
# запуска на исполнение. Есть способ это исправить, но об этом в следующих уроках.

# 6. Возвращает результат работы передаваемой корутины. asyncio.run() возвращает результат переданной корутины. В примере
# ниже корутина main() является точкой входа, именно внутри нее запускаются остальные корутины и задачи. asyncio.run() 
# в свою очередь возвращает результат выполнения main().
# import asyncio

# async def coroutine_1():
#     await asyncio.sleep(1)
#     print("Корутина 1 выполнена")

# async def coroutine_2():
#     await asyncio.sleep(2)
#     print("Корутина 2 выполнена")

# async def main():
#     task1 = asyncio.create_task(coroutine_1())
#     task2 = asyncio.create_task(coroutine_2())
#     await task1
#     await task2
#     return "Все корутины выполнены"


# # Точка входа программы
# result = asyncio.run(main())
# print(result)

# 7. Ошибка, вызванная в одной из корутин, будет проброшена в вызывающий код. Если в процессе выполнения функции main() 
# произошла ошибка, то asyncio.run(main()) гарантирует, что все остальные асинхронные операции будут отменены, и
# затем пробросит исключение в вызывающий код.
# import asyncio


# async def task_func(duration):
#     name = asyncio.current_task().get_name()
#     print(f'Задача {name} запущена, будет выполнена за {duration} секунд.')
#     await asyncio.sleep(duration)
#     print(f'Задача {name} завершена.')


# async def exсeptor(duration):
#     await asyncio.sleep(duration)
#     # Здесь возникает исключение
#     print(f'Задача {asyncio.current_task().get_name()} вызвала ошибку через {duration} секунды')
#     raise Exception("Произошла ошибка в main()")


# async def main():
#     task1 = asyncio.create_task(task_func(3), name='first')
#     task2 = asyncio.create_task(task_func(1), name='second')
#     task3 = asyncio.create_task(exсeptor(2), name='exсeption')
#     await asyncio.gather(task1, task2, task3)


# try:
#     asyncio.run(main())
# except Exception as e:
#     print(f'Было поднято исключение: {e}')

# Обратите внимание на то, что время вызова исключения находится между завершением первой и второй задачи.
# После вызова исключения задача с именем first не сообщит о своем завершении.
# Вывод:
# Задача first запущена, будет выполнена за 3 секунд.
# Задача second запущена, будет выполнена за 1 секунд.
# Задача second завершена.
# Задача exсeption вызвала ошибку через 2 секунды
# Было поднято исключение: Произошла ошибка в main()

# Вы можете столкнутся с подобной проблемой в  следующих случаях:

# Сетевые запросы: при выполнении асинхронных сетевых запросов может произойти исключение (например, если сервер недоступен).
# При этом важно, чтобы все остальные асинхронные запросы были завершены корректно.

# Асинхронные операции ввода/вывода: при работе с асинхронными операциями ввода/вывода (например, чтение/запись файлов) 
# также может произойти исключение (например, если файл не найден). В этом случае необходимо гарантировать, что все другие операции
# ввода/вывода будут завершены корректно, чтобы не потерять данные и не заблокировать ресурсы.

# Обработка ошибок и исключений: в некоторых случаях вы можете обработать ошибку, тем самым позволив вашему коду продолжить
# выполнение работы после возникновения исключения. В таком случае даже при возникновении исключения все ваши задачи будут корректно завершены.


#Для того, чтобы обработать исключение, необходимо добавить блок try/except на потенциально опасном участке кода.

import asyncio


async def task_func(duration):
    name = asyncio.current_task().get_name()
    print(f'Задача {name} запущена, будет выполнена за {duration} секунд.')
    await asyncio.sleep(duration)
    print(f'Задача {name} завершена.')


async def exсeptor(duration):
    # Перехватываем исключение в опасном месте
    try:
        await asyncio.sleep(duration)
        # Здесь возникает исключение
        print(f'Задача {asyncio.current_task().get_name()} вызвала ошибку через {duration} секунды')
        raise Exception("Произошла ошибка")
    except Exception as e:
        print(f'При выполнении задачи {asyncio.current_task().get_name()} было поднято исключение: {e}')

async def main():
    task1 = asyncio.create_task(task_func(3), name='first')
    task2 = asyncio.create_task(task_func(1), name='second')
    task3 = asyncio.create_task(exсeptor(2), name='exсeption')
    await asyncio.gather(task1, task2, task3)

asyncio.run(main())

# Вывод:

# Задача first запущена, будет выполнена за 3 секунд.
# Задача second запущена, будет выполнена за 1 секунд.
# Задача second завершена.
# Задача exсeption вызвала ошибку через 2 секунды
# При выполнении задачи exсeption было поднято исключение: Произошла ошибка
# Задача first завершена.

# Изменения в версии 3.12: Добавлен loop_factory параметр.
# loop_factory — это параметр, который позволяет пользователю указать фабрику для создания нового экземпляра цикла
# событий, который будет использоваться в asyncio.run(). Этот параметр предназначен для расширенной настройки и 
# тестирования, позволяя программистам определить, как именно должен быть создан и сконфигурирован цикл событий.


# По умолчанию, если параметр loop_factory не указан (т.е., None), asyncio.run() автоматически создает новый цикл событий,
# используя стандартную реализацию цикла событий, которая полностью управляется библиотекой asyncio. Этот вариант
# подходит для большинства приложений, где не требуется специальная конфигурация цикла.

# Использование пользовательской loop_factory позволит вам контролировать создание и настройку цикла событий. Этот
# вариант может быть полезен для тестирования, когда необходимо имитировать определенные условия, или в ситуациях,
# когда требуется настроить цикл событий определенным образом, например, для интеграции с другими библиотеками или
# для оптимизации производительности.


# Для создания loop_factory вы должны определить функцию или класс, который возвращает экземпляр asyncio.AbstractEventLoop.

# Пример создания простой пользовательской фабрики цикла событий:

import asyncio

def custom_loop_factory():
    loop = asyncio.new_event_loop()
    # Здесь могут быть добавлены настройки, специфичные для вашего приложения
    return loop

#Для использования loop_factory в asyncio.run(), просто передайте вашу фабрику в качестве аргумента при вызове функции:
async def main():
    # Ваш асинхронный код здесь
    pass

# Запуск асинхронной программы с пользовательским циклом событий
asyncio.run(main(), loop_factory=custom_loop_factory)